#!/usr/bin/env python3
import requests
import re
import json
import os
import time
import random
from urllib.parse import urlparse, quote
import math


class BilibiliDownloader:
    def __init__(self):
        self.session = requests.Session()
        # 使用更真实的请求头
        self.base_headers = {
            "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
            "Referer": "https://www.bilibili.com/",
            "Accept": "application/json, text/plain, */*",
            "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
            "Accept-Encoding": "gzip, deflate, br",
            "Origin": "https://www.bilibili.com",
            "Sec-Fetch-Dest": "empty",
            "Sec-Fetch-Mode": "cors",
            "Sec-Fetch-Site": "same-site",
            "Connection": "keep-alive",
            "Cache-Control": "no-cache",
            "Pragma": "no-cache"
        }
        self.session.headers.update(self.base_headers)

        # 更多User-Agent
        self.user_agents = [
            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15",
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0",
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/121.0"
        ]
        
        # 清晰度映射表 (qn参数)
        self.quality_map = {
            "1": {"name": "流畅 360P", "qn": 16},
            "2": {"name": "清晰 480P", "qn": 32},
            "3": {"name": "高清 720P", "qn": 64},
            "4": {"name": "高清 720P60", "qn": 74},
            "5": {"name": "高清 1080P", "qn": 80},
            "6": {"name": "高清 1080P+", "qn": 112},
            "7": {"name": "高清 1080P60", "qn": 116},
            "8": {"name": "超清 4K", "qn": 120},
            "9": {"name": "原画", "qn": 125},
            "10": {"name": "HDR", "qn": 126},
            "11": {"name": "杜比视界", "qn": 127}
        }

    def get_random_headers(self, referer=None):
        """获取随机请求头"""
        headers = self.base_headers.copy()
        headers["User-Agent"] = random.choice(self.user_agents)
        if referer:
            headers["Referer"] = referer
        return headers

    def search_videos_simple(self, keyword):
        """
        简单但可靠的搜索方法 - 使用B站移动端API
        """
        try:
            print(f"🔍 正在搜索: {keyword}...")

            # 使用B站移动端API，反爬机制较弱
            search_url = "https://api.bilibili.com/x/web-interface/search/type"

            params = {
                "search_type": "video",
                "keyword": keyword,
                "page": 1,
                "page_size": 10,  # 减少数量提高成功率
                "order": "totalrank",
                "duration": 0,
                "tids": 0
            }

            headers = self.get_random_headers("https://www.bilibili.com/")

            # 添加延迟
            time.sleep(random.uniform(2, 3))

            response = self.session.get(search_url, params=params, headers=headers, timeout=20)

            if response.status_code != 200:
                print(f"❌ 搜索请求失败，状态码: {response.status_code}")
                return []

            data = response.json()

            if data.get("code") != 0:
                print(f"❌ 搜索API返回错误: {data.get('message')}")
                return []

            results = data.get("data", {}).get("result", [])
            video_list = []

            for i, item in enumerate(results, 1):
                video_info = {
                    "index": i,
                    "title": self.clean_text(item.get("title", "未知标题")),
                    "bvid": item.get("bvid", ""),
                    "aid": item.get("id", ""),
                    "author": item.get("author", "未知作者"),
                    "duration": item.get("duration", "未知时长"),
                    "play_count": self.format_number(item.get("play", 0)),
                    "danmaku_count": self.format_number(item.get("danmaku", 0)),
                    "url": f"https://www.bilibili.com/video/{item.get('bvid', '')}",
                    "description": self.clean_text(item.get("description", ""))[:100]
                }
                video_list.append(video_info)

            print(f"✅ 找到 {len(video_list)} 个视频")
            return video_list

        except Exception as e:
            print(f"❌ 搜索失败: {e}")
            return []

    def search_videos_fallback(self, keyword):
        """
        改进的备用搜索方法 - 使用更精确的匹配策略
        """
        try:
            print("🔄 使用改进的备用搜索方法...")

            # 直接访问网页版搜索
            search_url = f"https://search.bilibili.com/all"
            params = {
                "keyword": keyword,
                "from_source": "webtop_search",
                "spm_id_from": "333.1007",
                "search_source": "5"
            }

            headers = {
                "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
                "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
                "Accept-Language": "zh-CN,zh;q=0.9",
                "Referer": "https://www.bilibili.com/",
                "Cache-Control": "no-cache"
            }

            response = requests.get(search_url, params=params, headers=headers, timeout=20)
            response.raise_for_status()

            video_list = []
            
            # 方法1: 使用更精确的正则表达式匹配视频标题和BV号
            # 匹配包含标题和BV号的完整结构
            pattern = r'<a[^>]*title="([^"]*)"[^>]*href="//www\.bilibili\.com/video/(BV[^"]+)"'
            matches = re.findall(pattern, response.text)
            
            seen_bvids = set()
            for title, bvid in matches:
                if bvid not in seen_bvids and len(title) > 5:
                    seen_bvids.add(bvid)
                    video_info = {
                        "index": len(seen_bvids),
                        "title": self.clean_text(title),
                        "bvid": bvid,
                        "author": "未知作者",
                        "duration": "未知时长",
                        "play_count": "未知",
                        "danmaku_count": "未知",
                        "url": f"https://www.bilibili.com/video/{bvid}",
                        "description": ""
                    }
                    video_list.append(video_info)
                    if len(video_list) >= 10:
                        break

            # 方法2: 如果方法1失败，尝试匹配包含标题的div和BV号
            if not video_list:
                print("⚠️  尝试方法2匹配...")
                # 匹配标题
                title_pattern = r'<div class="title">[^<]*<a[^>]*>([^<]+)</a>'
                titles = re.findall(title_pattern, response.text)
                
                # 匹配BV号
                bv_pattern = r'href="//www\.bilibili\.com/video/(BV[0-9A-Za-z]+)"'
                bvids = re.findall(bv_pattern, response.text)
                
                # 尝试将标题和BV号对应
                min_length = min(len(titles), len(bvids))
                for i in range(min_length):
                    if i < 10:  # 只取前10个
                        video_info = {
                            "index": i + 1,
                            "title": self.clean_text(titles[i]),
                            "bvid": bvids[i],
                            "author": "未知作者",
                            "duration": "未知时长",
                            "play_count": "未知",
                            "danmaku_count": "未知",
                            "url": f"https://www.bilibili.com/video/{bvids[i]}",
                            "description": ""
                        }
                        video_list.append(video_info)

            # 方法3: 如果还是失败，使用最简单的BV号匹配
            if not video_list:
                print("⚠️  使用方法3匹配...")
                bv_pattern = r'/video/(BV[0-9A-Za-z]+)'
                bv_matches = re.findall(bv_pattern, response.text)
                
                # 去重
                unique_bvids = []
                for bvid in bv_matches:
                    if bvid not in unique_bvids:
                        unique_bvids.append(bvid)
                
                # 为每个BV号获取标题
                for i, bvid in enumerate(unique_bvids[:10], 1):
                    # 尝试获取真实标题
                    title = self.get_video_title_by_bvid(bvid)
                    if not title:
                        title = f"视频 {bvid}"
                    
                    video_info = {
                        "index": i,
                        "title": title,
                        "bvid": bvid,
                        "author": "未知作者",
                        "duration": "未知时长",
                        "play_count": "未知",
                        "danmaku_count": "未知",
                        "url": f"https://www.bilibili.com/video/{bvid}",
                        "description": ""
                    }
                    video_list.append(video_info)

            print(f"✅ 通过网页搜索找到 {len(video_list)} 个视频")
            return video_list

        except Exception as e:
            print(f"❌ 备用搜索失败: {e}")
            return []

    def get_video_title_by_bvid(self, bvid):
        """通过BV号获取视频标题"""
        try:
            url = f"https://www.bilibili.com/video/{bvid}"
            headers = self.get_random_headers(url)
            response = requests.get(url, headers=headers, timeout=10)
            
            # 从页面中提取标题
            title_match = re.search(r'<title[^>]*>(.*?)</title>', response.text)
            if title_match:
                raw_title = title_match.group(1).split('_哔哩哔哩')[0]
                return self.clean_text(raw_title)
        except Exception as e:
            print(f"⚠️  获取视频标题失败: {e}")
        
        return None

    def search_videos(self, keyword):
        """
        主搜索函数 - 尝试多种方法
        """
        # 方法1: 尝试简单搜索
        videos = self.search_videos_simple(keyword)

        # 方法2: 如果简单搜索失败，尝试备用方法
        if not videos:
            videos = self.search_videos_fallback(keyword)

        return videos

    def clean_text(self, text):
        """清理文本"""
        if not text:
            return ""
        # 移除HTML实体
        text = re.sub(r'&[a-z]+;', '', text)
        # 移除多余空格
        text = re.sub(r'\s+', ' ', text).strip()
        return text

    def format_number(self, num):
        """格式化数字显示"""
        try:
            num = int(num)
            if num >= 10000:
                return f"{num / 10000:.1f}万"
            return str(num)
        except:
            return str(num)

    def display_search_results(self, videos):
        """显示搜索结果"""
        if not videos:
            print("❌ 未找到相关视频")
            return False

        print("\n" + "=" * 80)
        print("📺 搜索结果:")
        print("=" * 80)

        for video in videos:
            print(f"{video['index']:2d}. {video['title']}")
            print(f"    👤 {video['author']} | ⏱️ {video['duration']} | "
                  f"📊 播放: {video['play_count']} | 💬 弹幕: {video['danmaku_count']}")
            if video.get('description'):
                print(f"    📝 {video['description']}")
            print(f"    🔗 {video['url']}")
            print("-" * 80)

        return True

    def get_available_qualities(self, play_info):
        """获取视频可用的清晰度"""
        available_qualities = []
        
        # 检查DASH格式
        if play_info.get('data', {}).get('dash'):
            dash_data = play_info['data']['dash']
            if dash_data.get('video'):
                for video_stream in dash_data['video']:
                    qn = video_stream.get('id')
                    bandwidth = video_stream.get('bandwidth', 0)
                    codecs = video_stream.get('codecs', '')
                    
                    # 查找对应的清晰度名称
                    quality_name = "未知"
                    for q_info in self.quality_map.values():
                        if q_info['qn'] == qn:
                            quality_name = q_info['name']
                            break
                    
                    available_qualities.append({
                        'qn': qn,
                        'name': quality_name,
                        'bandwidth': bandwidth,
                        'codecs': codecs
                    })
        
        # 检查传统格式
        elif play_info.get('data', {}).get('durl'):
            # 传统格式通常只有一种清晰度
            qn = play_info['data'].get('quality', 16)  # 默认为360P
            quality_name = "传统格式"
            for q_info in self.quality_map.values():
                if q_info['qn'] == qn:
                    quality_name = q_info['name']
                    break
            
            available_qualities.append({
                'qn': qn,
                'name': quality_name,
                'bandwidth': 0,
                'codecs': '未知'
            })
        
        # 按带宽排序（带宽越高，画质越好）
        available_qualities.sort(key=lambda x: x['bandwidth'], reverse=True)
        return available_qualities

    def get_video_info(self, url_or_bvid):
        """获取视频信息，包括可用清晰度"""
        # 如果是BV号，构造完整URL
        if url_or_bvid.startswith("BV"):
            url = f"https://www.bilibili.com/video/{url_or_bvid}"
        else:
            url = url_or_bvid

        try:
            # 验证URL格式
            if not self.is_valid_bilibili_url(url):
                print("❌ 请输入正确的B站视频URL或BV号")
                return None

            # 添加随机延迟
            time.sleep(random.uniform(1, 2))

            # 获取页面HTML
            print("📄 获取视频页面...")
            headers = self.get_random_headers(url)
            response = self.session.get(url, headers=headers, timeout=15)
            response.raise_for_status()

            if response.status_code != 200:
                print(f"❌ 页面请求失败，状态码: {response.status_code}")
                return None

            # 提取视频信息
            print("🔍 解析视频信息...")

            # 方法1：从JavaScript变量中提取（优先）
            pattern = r'window\.__playinfo__\s*=\s*({.*?})</script>'
            match = re.search(pattern, response.text)

            if match:
                try:
                    play_info = json.loads(match.group(1))
                    print("✅ 通过playinfo获取视频数据")
                except json.JSONDecodeError:
                    print("❌ playinfo JSON解析失败")
                    play_info = None
            else:
                play_info = None

            # 方法2：如果方法1失败，尝试其他方式
            if not play_info:
                print("⚠️  尝试备用方法提取视频信息...")
                pattern = r'"aid":(\d+),.*?"bvid":"(BV.*?)",.*?"cid":(\d+)'
                match = re.search(pattern, response.text)
                if match:
                    aid, bvid, cid = match.groups()
                    print(f"📋 获取到参数: AID={aid}, BVID={bvid}, CID={cid}")

                    # 调用API获取视频信息，使用最高质量
                    api_url = f"https://api.bilibili.com/x/player/playurl?avid={aid}&bvid={bvid}&cid={cid}&qn=125&fnval=16"
                    api_headers = self.get_random_headers(url)
                    api_response = self.session.get(api_url, headers=api_headers, timeout=15)
                    play_info = api_response.json()

                    if play_info.get('code') != 0:
                        print(f"❌ API请求失败: {play_info.get('message')}")
                        return None
                else:
                    print("❌ 无法提取视频信息，可能页面结构已更新")
                    return None

            # 获取视频标题
            title_match = re.search(r'<title[^>]*>(.*?)</title>', response.text)
            if title_match:
                raw_title = title_match.group(1).split('_哔哩哔哩')[0]
                title = self.clean_text(raw_title)
            else:
                title = "未知标题"

            # 获取可用清晰度
            available_qualities = self.get_available_qualities(play_info)

            return {
                'title': title,
                'play_info': play_info,
                'available_qualities': available_qualities,
                'url': url
            }

        except Exception as e:
            print(f"❌ 获取视频信息失败: {e}")
            return None

    def download_bilibili_video(self, url_or_bvid):
        """
        下载B站视频（支持URL或BV号）
        """
        # 先获取视频信息
        video_info = self.get_video_info(url_or_bvid)
        if not video_info:
            return False

        play_info = video_info['play_info']
        available_qualities = video_info['available_qualities']
        title = video_info['title']
        url = video_info['url']

        # 显示可用清晰度并自动选择最高质量
        print(f"\n📺 视频标题: {title}")
        print("📊 可用清晰度:")
        for i, q in enumerate(available_qualities, 1):
            bandwidth_info = f", 码率: {q['bandwidth']//1000}Kbps" if q['bandwidth'] > 0 else ""
            print(f"  {i}. {q['name']} (qn={q['qn']}{bandwidth_info})")
        
        # 自动选择最高质量
        best_quality = available_qualities[0]['qn']
        print(f"🎯 自动选择最高可用清晰度: {available_qualities[0]['name']}")

        # 提取视频下载链接
        video_url = None
        audio_url = None

        # 优先使用DASH格式（高画质）
        if play_info.get('data', {}).get('dash'):
            dash_data = play_info['data']['dash']
            
            # 选择最高质量的视频流
            if dash_data.get('video'):
                for video_stream in dash_data['video']:
                    if video_stream.get('id') == best_quality:
                        video_url = video_stream.get('baseUrl')
                        print(f"🎬 使用DASH格式视频流: {best_quality}")
                        break
                
                # 如果指定清晰度不可用，使用最高质量
                if not video_url:
                    video_stream = max(dash_data['video'], key=lambda x: x.get('bandwidth', 0))
                    video_url = video_stream.get('baseUrl')
                    actual_quality = video_stream.get('id')
                    print(f"🎬 使用最高可用DASH格式视频流: qn={actual_quality}")
            
            # 获取音频流
            if dash_data.get('audio'):
                audio_stream = max(dash_data['audio'], key=lambda x: x.get('bandwidth', 0))
                audio_url = audio_stream.get('baseUrl')
                print("🔊 获取到音频流")

        # 备用：传统格式
        if not video_url and play_info.get('data', {}).get('durl'):
            video_url = play_info['data']['durl'][0]['url']
            print("🎬 使用传统格式视频流")

        if not video_url:
            print("❌ 无法找到可用的视频链接")
            return False

        print("🔗 获取到视频链接")

        # 生成文件名
        filename = self.clean_filename(title) + ".mp4"
        print(f"📝 视频标题: {title}")

        # 下载视频
        print("⬇️ 开始下载视频...")
        download_headers = self.get_random_headers("https://www.bilibili.com/")

        # 设置更长的超时时间
        video_response = self.session.get(video_url, headers=download_headers, stream=True, timeout=60)
        video_response.raise_for_status()

        # 获取文件大小
        total_size = int(video_response.headers.get('content-length', 0))
        if total_size == 0:
            print("⚠️  无法获取文件大小，可能影响进度显示")

        downloaded_size = 0
        start_time = time.time()

        # 确保下载目录存在
        download_dir = self.ensure_download_dir()
        filepath = os.path.join(download_dir, filename)

        with open(filepath, 'wb') as f:
            for chunk in video_response.iter_content(chunk_size=8192):
                if chunk:
                    f.write(chunk)
                    downloaded_size += len(chunk)

                    # 显示下载进度
                    if total_size > 0:
                        progress = (downloaded_size / total_size) * 100
                        elapsed_time = time.time() - start_time

                        # 计算下载速度
                        if elapsed_time > 0:
                            speed = downloaded_size / elapsed_time / 1024  # KB/s
                            remaining_time = (total_size - downloaded_size) / (
                                        downloaded_size / elapsed_time) if downloaded_size > 0 else 0

                            print(
                                f"\r📥 下载进度: {progress:6.2f}% | {self.format_file_size(downloaded_size)}/{self.format_file_size(total_size)} | 速度: {speed:6.2f}KB/s | 剩余: {remaining_time:4.0f}s",
                                end='', flush=True)

        print(f"\n✅ 下载完成！文件保存为: {filename}")
        print(f"📁 文件位置: {filepath}")
        
        # 提示音频信息
        if audio_url:
            print("💡 提示: 此视频使用DASH格式，需要单独下载音频并合并才能获得完整视频")
            print("💡 建议使用专业工具如ffmpeg进行音视频合并")
        
        return True

    def is_valid_bilibili_url(self, url):
        """验证是否为有效的B站视频URL"""
        return url.startswith(('https://www.bilibili.com/video/', 'https://b23.tv/', 'http://www.bilibili.com/video/'))

    def clean_filename(self, filename):
        """清理文件名"""
        illegal_chars = ['/', '\\', ':', '*', '?', '"', '<', '>', '|']
        for char in illegal_chars:
            filename = filename.replace(char, '_')
        # 移除多余空格和限制长度
        filename = re.sub(r'\s+', ' ', filename).strip()
        if len(filename) > 100:
            filename = filename[:100]
        return filename

    def format_file_size(self, size_bytes):
        """格式化文件大小显示"""
        if size_bytes == 0:
            return "0B"
        size_names = ["B", "KB", "MB", "GB"]
        i = 0
        while size_bytes >= 1024 and i < len(size_names) - 1:
            size_bytes /= 1024.0
            i += 1
        return f"{size_bytes:.2f}{size_names[i]}"

    def ensure_download_dir(self):
        """确保下载目录存在"""
        download_dir = os.path.join(os.path.expanduser("~"), "Downloads", "Bilibili")
        if not os.path.exists(download_dir):
            os.makedirs(download_dir)
        return download_dir


def main():
    downloader = BilibiliDownloader()

    while True:
        print("\n" + "=" * 50)
        print("🎬 B站视频下载器 v4.1 (智能画质选择)")
        print("=" * 50)
        print("1. 搜索视频并下载")
        print("2. 通过URL或BV号下载")
        print("3. 退出")

        choice = input("\n请选择操作 (1-3): ").strip()

        if choice == "1":
            # 搜索模式
            keyword = input("请输入搜索关键词: ").strip()
            if not keyword:
                print("❌ 关键词不能为空")
                continue

            videos = downloader.search_videos(keyword)

            if videos:
                downloader.display_search_results(videos)
                try:
                    selection = input("\n请选择要下载的视频编号 (输入0返回主菜单): ").strip()
                    if selection == "0":
                        continue

                    index = int(selection) - 1
                    if 0 <= index < len(videos):
                        selected_video = videos[index]
                        print(f"\n🎯 已选择: {selected_video['title']}")
                        print(f"🔗 下载URL: {selected_video['url']}")
                        
                        # 直接下载，让下载函数自动选择最佳画质
                        downloader.download_bilibili_video(selected_video['url'])
                    else:
                        print("❌ 无效的选择")
                except ValueError:
                    print("❌ 请输入有效的数字")
            else:
                print("❌ 搜索失败，未找到相关视频")
                # 搜索失败时提供手动输入选项
                manual_input = input("\n是否手动输入视频URL或BV号? (y/N): ").strip().lower()
                if manual_input == 'y':
                    url_or_bvid = input("请输入B站视频URL或BV号: ").strip()
                    if url_or_bvid:
                        downloader.download_bilibili_video(url_or_bvid)

        elif choice == "2":
            # 直接下载模式
            url_or_bvid = input("请输入B站视频URL或BV号: ").strip()
            if url_or_bvid:
                downloader.download_bilibili_video(url_or_bvid)
            else:
                print("❌ URL或BV号不能为空")

        elif choice == "3":
            print("👋 再见！")
            break

        else:
            print("❌ 无效的选择，请重新输入")


if __name__ == "__main__":
    main()
